Skip to content

Blog/kagent security lab - #264

Open
Prianshu-git wants to merge 1 commit into
kubesimplify:mainfrom
Prianshu-git:blog/kagent-security-lab
Open

Prianshu-git wants to merge 1 commit into
kubesimplify:mainfrom
Prianshu-git:blog/kagent-security-lab

Conversation

@Prianshu-git

Copy link
Copy Markdown
Contributor

Fixes Issue

Changes proposed

Screenshots

Note to reviewers

@saiyam1814 saiyam1814 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Prianshu-git, thanks for this one. I went through it properly, checked the kagent API claims against the source, and pulled up all five images. Short version: the technical substance is solid and this is close, closer than most first drafts we get. There's a handful of things to fix before it goes out, and two of them would actually block a reader from following along.

First, credit where it's due, because I verified this rather than assuming it. requireApproval is a real field on McpServerTool in kagent.dev/v1alpha2, nested exactly where you put it (sibling of toolNames, under mcpServer), and kagent enforces a CEL rule that every entry in requireApproval must also appear in toolNames. Your YAML satisfies that. The ModelConfig shape checks out too (model, provider, ollama.host are all real fields), and chart version 0.9.12 exists. That matters a lot to me, because the last blog I reviewed had a centerpiece YAML field that didn't exist in the project at all. Yours holds up.

The lab also reads like you genuinely ran it. The stalled ollama pull progress line sitting at 94% with 16 KB/s and a 58m29s ETA is not something anyone invents, and neither is "I switched models during the lab". Keep that texture, it's the best thing about the post.

Two things that block a reader

There's no way to open the dashboard. Step 6 says "Open the kagent dashboard at http://localhost:8080", but nothing anywhere in the post starts a port-forward. A reader following along hits that line and stops. kagent's own CLI prints this when you run kagent dashboard:

kubectl port-forward -n kagent service/kagent-ui 8082:8080

Note the port: 8080 is the in-cluster service port, and the local one is 8082, so http://localhost:8080 is also wrong as written. Please add the port-forward as its own step before Step 6 and fix the URL.

The lab files are never obtained. Step 1 opens with kind create cluster --config 00-cluster/kind-config.yaml, and later steps reference 01-local-llm/ollama-deployment.yaml, 01-local-llm/modelconfig.yaml, 02-first-agent/agent.yaml and 03-human-in-the-loop/hitl-agent.yaml. None of those exist on the reader's machine, and the repo that has them is linked in the very last line of the post. Please add a clone step in the prerequisites:

git clone https://github.com/Prianshu-git/Kagent-demo
cd Kagent-demo

I'd also inline kind-config.yaml since it's the very first command and it's presumably short. Everything else can stay as a repo reference.

The images, which is where the most upside is

You committed five images but only reference three. 1.png and 3.png are sitting in the PR unused, and 3.png in particular is the single best screenshot in the set. Let me explain why.

3.png shows local-k8s-agent being asked "how many namespaces currently in my cluster". It calls k8s_get_available_api_resources and answers "There are currently 51 namespaces in your cluster." That answer is wrong twice over: a kind cluster with kagent and Ollama on it has something like seven namespaces, and the tool it reached for lists API resource types, not namespaces. The model grabbed the wrong tool and then reported that tool's item count as a namespace count.

That is a perfect, real illustration of the exact point you make in "Notes on model selection and behavior", and right now that section has no evidence behind it. Put 3.png there. It's much more convincing than the prose alone.

It also complicates a claim you make a few lines later, and I think honestly makes the post better: you write that k8s_get_resources "doesn't hallucinate. If the agent uses it, the answer is real." Your own screenshot shows the failure mode is upstream of that, the model picking the wrong tool in the first place. Worth softening to something like: the tools return ground truth, but nothing guarantees the model calls the right one, which is exactly why read-only scoping and approval gates matter.

Two more image notes:

  • 2.png is the wrong agent for where it sits. It's placed in Step 6 under the read-only agent with alt text "local-K8s-agent", but the screenshot clearly shows kagent/local-hitl-agent answering. 3.png is the one that actually shows local-k8s-agent. Simplest fix: use 3.png's sibling framing for Step 6, or just re-take the read-only screenshot.
  • Steps 7 and 8 have no screenshots at all, and they're the whole point of the post. The approval gate pausing in the UI, and you clicking approve, then reject, is the money shot of this entire lab. 1.png is close but shows a read response rather than a pending approval. If you can, grab two: one of the pending-approval state, one after rejection. That single addition would lift the post more than any prose change.

On file names and weight: 1.png, 2.png, 3.png, cover1.png should be descriptive (hitl-approval-pending.png and so on), and the two big ones need compressing. cover1.png is 1.9MB and Architecture.png is 1.2MB. Aim for under ~300KB each, WebP is fine.

On the cover image specifically: it's clearly AI-generated, and it shows. The laptop UI in it has garbled nonsense words down the left sidebar where menu labels should be. That's the classic tell, and readers spot it instantly on a post whose whole subject is trusting AI systems. My honest suggestion: drop it and use a clean screenshot of the actual approval gate as the cover. A real UI showing "Pending approval" with Approve and Reject buttons is a stronger, more credible image for this post than any generated art. If you'd rather keep generated art, at least regenerate it without legible-but-fake text.

The architecture diagram, on the other hand, is genuinely good. Clear, accurate to the lab, nicely on-brand. One nit: it shows an "RBAC-scoped" badge, but Part 1 doesn't cover RBAC at all (you've scoped that to Part 2), so either drop that badge or note it's coming next.

Accuracy details

Your performance numbers contradict each other three ways. Step 4 says qwen2.5:1.5b is "roughly 5x faster than larger alternatives". Later the same section says "roughly 5-8x faster". And the concrete figures given, 2 to 4 tokens/sec versus 0.25 tokens/sec, work out to 8x to 16x. Pick the number your measurements actually support and use it consistently. I'd also double-check the 0.25 t/s figure for llama3.2:3b, since that's unusually slow even for CPU-only inference and it's the weakest-looking claim in the post. If it's real, say what the CPU and memory limits on the pod were, and it becomes credible instead of surprising.

Show where the timing logs come from. You paste slot print_timing lines but never show the command that produced them. Add the kubectl logs -n ollama deploy/ollama line above that block so readers can reproduce it.

The "Current cluster status" output doesn't match a fresh lab. The pod ages read 13h, 14h and 17h, which tells the reader this came from your long-running dev cluster rather than the walkthrough they just did. Someone following along will see ages in minutes. Either re-capture it from a clean run or add a line acknowledging it's from a cluster that had been up a while.

Step 2's wait may fire too early. kubectl wait --for=condition=ready pod --all -n kagent immediately after helm install can fail with "no matching resources" because the pods don't exist yet. Worth either adding a short sleep, or waiting on the deployment instead.

Repo hygiene

This PR adds redirect rules for a post that doesn't exist. Your regenerated vercel.json and public/_redirects include entries for zero-trust-istio-sidecar-vs-ambient, which isn't in this branch and isn't in main. That looks like you ran the generator while another post was sitting in your working tree. Merging as-is would publish a rewrite rule pointing at a page that 404s, and we've been bitten by exactly that class of bug before (it caused a refresh loop on the site). Please regenerate the redirects with only your post present:

git checkout origin/main -- vercel.json public/_redirects public/_worker.js
node scripts/generate-redirects.mjs

The filename and slug don't match. The file is kagent-security-lab.md but the frontmatter slug is kagent-part-1-local-ai-agent-kubernetes. Our guide asks for these to be identical, otherwise the automatic "Edit this page on GitHub" link 404s. The slug is the better name, so rename the file to kagent-part-1-local-ai-agent-kubernetes.md (and the image folder can stay as-is or move to match, your call).

You're not in content/authors.json. The frontmatter says author: prianshu-mukherjee but there's no such key, so the post would publish under the generic "Kubesimplify" byline instead of yours. Add yourself, per Step 3 of WRITING-A-BLOG.md, with an avatar in public/img/authors/.

Commits. There's a merge commit in the branch, and the DCO sign-off is on only one of the two commits. Squash into a single signed-off commit:

git reset --soft origin/main
git commit -s -m "Add blog post: kagent Part 1, local Kubernetes-native AI agent with human-in-the-loop approval"
git push --force

Style and copy

  • Two em dashes need replacing (we don't use them): the seoDescription ("with Ollama — read-only") and the prerequisites line ("Keep the model small — this walkthrough"). There's also an en dash in "45–60 minutes" on the reading-time line.
  • Drop the reading-time line entirely. The site calculates and displays reading time automatically from the content, so "Reading time: ~18 minutes" will contradict it. Keep the hands-on estimate if you like, just move it into the intro prose.
  • Seven tags, we ask for two to five. I'd cut to ["kagent", "kubernetes", "ai-agents", "human-in-the-loop"].
  • Typos and small grammar: "explain situtation" (missing word and typo), "That's no a bug" should be "not", "posture don't grant a tool" needs a colon or dash, "proceeding on guesses make things even worse" (subject-verb), "this lab demonstrated one thing a local AI agent can operate" needs a colon after "one thing", "The architecture is explicit :" and "qwen2.5:1.5b ." have stray spaces before punctuation, and "llama 3.2:3b" is written three different ways across the post (pick llama3.2:3b).
  • One structural repeat: the three-bullet list right after the intro ("An AI chatbot can explain... A kagent agent can decide what to inspect next...") restates the opening paragraph almost word for word. Cut one of them.

Where it lands

The bones are good and the technical claims hold, which is the hard part. What it needs is the two reader-blocking gaps closed (port-forward, clone step), the unused screenshots put to work (especially 3.png in the hallucination section), a real screenshot of the approval gate in Steps 7 and 8, the redirect regeneration, and the copy pass.

Do that and I'm glad to publish it. Ping me when you've pushed and I'll spin up a preview and take another look.

@Prianshu-git
Prianshu-git force-pushed the blog/kagent-security-lab branch from 82da0eb to 2fa3f0d Compare September 2, 2026 10:53
@Prianshu-git

Prianshu-git commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Hey @saiyam1814, thanks again for the detailed and constructive review.

I’ve addressed the feedback and pushed the updated changes. The commit includes the blog Markdown, all 11 Kagent images and the regenerated feed/redirect files.

I also rebased onto the latest upstream/main, regenerated the redirects with the current generator, ran the production build to regenerate the feed files, and pushed everything as a single DCO-signed commit.

authors.json wasn’t changed because my author entry was already present and correctly formatted.

Could you please take another look and let me know if you notice any remaining inconsistencies, technical inaccuracies or anything else that should be corrected before publishing?

Thanks again for the thorough review!

P.S. I noticed a few minor formatting and grammatical issues slipped through the final pass. I’ll clean those up in a small follow-up commit shortly, if that’s okay.

@saiyam1814 saiyam1814 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really solid turnaround @Prianshu-git. I went through the whole thing again and every blocker from last round is closed.

Confirmed fixed: the file is renamed to match the slug, Step 6 port-forwards to the right port, the clone step is in the prerequisites, the reading-time line is gone, all 11 images are descriptively named with zero orphans and zero broken links, and the performance numbers are now internally consistent instead of contradicting themselves three ways. I also regenerated the redirect files from scratch on your branch and got an empty diff, so the generated files are correct and the stray zero-trust entries from last time are gone. You're already in authors.json from the Istio post, so your byline resolves.

Two things you did better than I asked for. Using the pending-approval screenshot as the cover is a much stronger choice than the generated art, since it shows the actual thing the post is about. And you used the hallucination screenshot exactly right: calling out that the model grabbed k8s_get_available_api_resources and reported that tool's item count as a namespace count, rather than inventing a number, is a sharper observation than "small models hallucinate." Softening the "it doesn't hallucinate" line to point at tool selection as the real risk surface is the correct nuance, and it makes the least-privilege argument land harder.

Adding the pod limits (2 vCPU / 4Gi) was the detail I most wanted, because it explains the timings. Which brings me to the one substantive thing I'd still change.

The one thing I'd still change: the performance framing

The numbers are real, I'm not questioning them. My problem is with what the post concludes from them, and there's a specific measurement that would settle it.

An 8x to 16x gap between a 1.5B and a 3B model is too large to be a parameter-count story. Twice the parameters means roughly twice the compute per token, so you'd expect something near 2x on the same cores. 8x to 16x says something else was going on, and the post currently attributes it to capability ("slightly lower reasoning capability", "if you need deeper reasoning at the cost of latency, switch back to llama3.2:3b"). That framing tells readers the slow model was thinking harder, when the likelier story is that it was starved.

The most probable culprit is right there in your own output: you have two ModelConfigs, default-model-config on llama3.2:3b and local-model-config on qwen2.5:1.5b, and your ollama list shows llama3.2:latest and llama3.2:3b too. If more than one model was resident at once, that's 3GB+ of weights alone inside a 4Gi limit, before KV cache and runtime overhead. Your own troubleshooting section even mentions the Ollama pod hitting memory pressure. The other candidate is simply the 2 vCPU ceiling.

I don't want to hand you my guess as fact, so here's how to find out. This takes about ten minutes.

1. See what's actually loaded. This is the diagnostic that matters most, and it also tells you whether inference ran on CPU or something else:

kubectl exec -n ollama deploy/ollama -- ollama ps

If that lists more than one model, you've found your answer. SIZE is the resident footprint and PROCESSOR confirms CPU.

2. Check whether the pod was being squeezed:

kubectl top pod -n ollama
kubectl get pod -n ollama -l app=ollama \
  -o jsonpath='{.items[0].status.containerStatuses[0].restartCount}{"\n"}'
kubectl describe pod -n ollama -l app=ollama | grep -A3 "Last State"

A non-zero restart count or an OOMKilled last state proves memory pressure outright.

3. Benchmark each model cleanly, one resident at a time. This sidesteps the agent loop entirely, so you're measuring the model rather than the whole system:

kubectl exec -n ollama deploy/ollama -- ollama stop llama3.2:3b
kubectl exec -n ollama deploy/ollama -- ollama run --verbose qwen2.5:1.5b \
  "Write exactly 100 words about Kubernetes scheduling."

kubectl exec -n ollama deploy/ollama -- ollama stop qwen2.5:1.5b
kubectl exec -n ollama deploy/ollama -- ollama run --verbose llama3.2:3b \
  "Write exactly 100 words about Kubernetes scheduling."

--verbose prints prompt eval rate and eval rate in tokens/sec. Same prompt, one model in memory at a time, so it's an apples-to-apples number you can publish with confidence.

4. Then raise the limits and run step 3 again:

kubectl set resources deployment/ollama -n ollama \
  --limits=cpu=6,memory=8Gi --requests=cpu=2,memory=4Gi
kubectl rollout status deployment/ollama -n ollama

Whatever comes back, you have a better post. If the gap collapses to roughly 2x once only one model is resident, you've got a genuinely interesting finding: on a memory-capped node the model that fits wins regardless of which one is smarter, which is a far more useful lesson than "bigger is slower." If the gap holds at 8x, then you've verified your original claim properly and can state it with the measurement behind it.

For the two places the post makes the claim, here's the shape I'd use once you have numbers. Step 4:

Why qwen2.5:1.5b? It's less than half the size of llama3.2:3b (986 MB vs. 2.0 GB), which matters more than raw parameter count on a memory-capped node. Measured with ollama run --verbose inside the pod, one model resident at a time, at limits of N vCPU / _N_Gi: qwen2.5:1.5b generated N tokens/sec versus N tokens/sec for llama3.2:3b. For structured tool calling, which is what agents actually need, the smaller model is more than adequate.

And in the performance section, swap the "honest tradeoff of local CPU inference" line for something that separates the two variables, along the lines of: these numbers come from a pod capped at N vCPU / _N_Gi, raising those limits changes them substantially, and the floor you can't tune away is that each interaction costs several full model passes.

Also worth adding: tell readers what wait to expect. "You probably noticed each interaction took a while" is honest but not actionable. Your own screenshots have the numbers: the read-only pod-listing answer used 1,183 output tokens, so at 2 to 4 tokens/sec that single answer took five to ten minutes. The approval interaction was lighter at 144 tokens, about a minute. Across Step 7 and the four parts of Step 9 that's roughly 15 to 40 minutes of waiting on top of the installs and the model pull. Put that in the prerequisites and nobody wonders whether their cluster has hung.

And name the host, since none of the timings are reproducible without it:

system_profiler SPHardwareDataType | grep -E "Chip|Total Number of Cores|Memory:"
docker info --format 'Docker: {{.NCPU}} CPUs, {{.MemTotal}} bytes'

One line with the chip, core count, RAM, and Docker's allocation is all it needs.

To be clear about where I land: none of this undermines the lab. What you're teaching (the approval boundary, least-privilege tool scoping, the CRD shape) behaves identically at 2 tokens/sec and 200 tokens/sec, you just wait longer to watch it work. The post is right that fully local is workable and not low-latency. I'd just rather it pin the latency on limits the reader can change than on CPU inference in general, because the first is a knob and the second sounds like a dead end.

Three small things

  • architecture-diagram.png is 1.5MB, now the only oversized asset in the post. Everything else is comfortably under 150KB. Worth getting it under ~300KB, and since it's a flat-colour diagram it should compress hard (WebP or SVG both work well here).
  • The publish date is still 2026-08-18. If we merge this week it'll land backdated and sort below newer posts. Bump it to the day we publish.
  • Your DCO sign-off uses prianshu@prianshus-MacBook-Air.local, which is your local machine hostname rather than a real address. Some DCO checks reject that. Setting git config user.email to your GitHub address and re-signing (git commit --amend -s --no-edit) sorts it.

Fix the performance framing and compress that diagram and I'm happy to ship this. The lab itself is in good shape now, and the approval-gate walkthrough with real screenshots is the strongest part.

…uman-in-the-loop approval

Signed-off-by: Prianshu <priyannshurmukherjee10964@gmail.com>
@Prianshu-git
Prianshu-git force-pushed the blog/kagent-security-lab branch from 2fa3f0d to 712794c Compare September 8, 2026 09:39
@saiyam1814

Copy link
Copy Markdown
Member

@Prianshu-git did you make any changes for this one?

@saiyam1814

Copy link
Copy Markdown
Member

@Prianshu-git answering my own question from last week first: yes, you did push. Commit 712794c landed on 8 Sep and addresses the round-two review. I missed it, and this PR then sat for a month on that misunderstanding. Sorry about that.

I went through the branch properly. Everything from the last list is closed, and the performance work is the honest version I was hoping for.

Confirmed fixed

  • Host is named: Apple M4, 10 cores, 16 GB, Docker allocated 10 CPUs / ~7.75 GiB. Timings are reproducible now.
  • Limits are named (2 vCPU / 4Gi) and the framing is reversed correctly. "The immediate reason is resource pressure, not a claim that a model with half the parameters should be an order of magnitude faster" is exactly right, and the OOMKilled detail is what makes it credible.
  • You published the failed measurement. "The Llama check timed out and the pod was subsequently OOM-killed, so there is not a trustworthy Llama tokens/sec number to publish" plus "Do not turn that failed run into a speed ratio" is the correct call and the best writing in the post. Deleting the 8x-16x claim rather than defending it is the right instinct.
  • Reader wait budget added (line 51): 1,183 tokens, 5-10 minutes, 15-40 minutes across Steps 7 and 9. That's the actionable version.
  • architecture-diagram 1.5 MB → 138 KB as a jpg. Every asset is now under 155 KB.
  • Copy pass is clean: zero em dashes, zero en dashes, and all five typos I listed ("situtation", "That's no a bug", the stray spaces before punctuation, the llama 3.2 inconsistency) are gone.
  • DCO now signs as priyannshurmukherjee10964@gmail.com instead of the MacBook hostname, and your byline resolves in authors.json.

One blocker: the branch no longer merges

mergeable: CONFLICTING. Not your fault, it's elapsed time. Three posts have merged since you branched (#266, #268, #271), and every generated file you committed has moved on main:

lib/_blog-feed-data.js   public/_redirects   public/_worker.js   vercel.json
public/atom.xml          public/rss.xml      public/llms.txt     public/llms-full.txt

Your _redirects still has zero-trust-istio-sidecar-vs-ambient but not inside-kueue-... or kubernetes-observability-.... Rebase and regenerate:

git fetch upstream && git rebase upstream/main
git checkout upstream/main -- vercel.json public/_redirects public/_worker.js public/_routes.json
npm run blog:redirects
npm run build          # regenerates the feed files
git add -A && git commit --amend -s --no-edit && git push --force

Heads up: #272 adds a CI step that fails when these drift, so once it lands this becomes an automatic check rather than something I catch by eye.

One substantive thing: the 0.38 tokens/sec figure is measuring cold start

This is the only real content issue, and it's worth fixing because the post now disagrees with itself by about 5x.

Three numbers for the same model on the same pod:

source rate
ollama run --verbose (line 466) 0.38 t/s
slot print_timing from an agent run (lines 481-484) 1.92 - 2.18 t/s
reader time budget (line 51) 2 - 4 t/s

At 0.38 t/s that 1,183-token answer takes 52 minutes, not the 5-10 you promise readers. So the headline measurement contradicts both your own logs and your own guidance, with nothing reconciling them.

I think I know why, and it's my fault for how I worded the last review. Look at what the measurement captured:

eval count: 26 token(s)
eval duration: 1m8.797653711s

26 tokens. You run ollama stop immediately before, which evicts the model, so the next call reloads ~1 GB of weights from disk, and that load time lands inside a 26-token sample. You measured model load, not generation throughput. The print_timing numbers are mid-run at steady state, which is why they're 5x higher and agree with your lived experience of the lab.

My original suggestion was "Write exactly 100 words about Kubernetes scheduling" precisely to get a long enough generation to swamp load time; the prompt became "Explain Kubernetes pods in one concise sentence", which is 26 tokens. That's on me for not saying why the length mattered.

Two ways to fix, either is fine:

  1. Re-run with a longer generation. Keep ollama stop, but ask for ~200 tokens, and issue a short throwaway prompt first so the model is resident before the timed one. You should land near the 2 t/s your logs already show.
  2. Keep the number and caption it, e.g. "0.38 t/s over a 26-token sample taken straight after a model reload, so this is dominated by load time; steady-state generation in the agent runs above sits around 2 t/s." Cheaper, and arguably more interesting, because cold start genuinely is what a reader feels on their first prompt.

Whichever you pick, make the three numbers agree or explain why they differ.

Two small ones

  • hitl-approval-confirmed.png is committed but never referenced. You had zero orphans last round, so this crept in. Either use it in Step 7 next to the pending-approval shot (the before/after pair is nice) or drop the file.
  • datePublished is 2026-09-08. I'll bump it to the publish date on merge, no action needed.

Where this lands

The lab is in good shape and the technical claims hold. Rebase and regenerate, then either re-measure or caption the 0.38, and I'll merge it. Genuinely sorry this stalled on me for a month.

The thing worth saying: I asked you to go measure something that might have undercut your own post, and you came back with "the run OOM-killed, there is no trustworthy number, do not turn this into a ratio." That is harder to write than a clean 8x, and it's why the rest of the numbers in here are believable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants